unit phys;

interface

type
  TVec2f = record
    X, Y: Single;
  end;

type
  TWorld = class;
  TBody = class;
  TBodyLine = class;
  TBodyCircle = class;

  // Событие, которое происходит при пересечении одного объекта с другим
  //    World - мир, в котором произошло пересечение
  //    Body - тело, которое следует обработать
  //    Other - тело, с которым произошло пересечение (его обрабатывать не нужно)
  //    Point - точка пересечения
  // См. ниже примеры обработчиков
  TOnIntersection = procedure (World: TWorld; Body, Other: TBody; const Point: TVec2f);

  // Кратковременный толчок на тело: это ускорение и время воздействия
  //   Примеры: прыжки, отдача от оружия
  TImpact = record
    Acc: TVec2f;
    Time: Integer;
  end;

  // Физическое тело
  TBody = class
    // Мир, в котором находится тело
    FWorld: TWorld;
    // Является ли тело неподвижным (например, для стен ставится в True)
    // Для неподвижных тел World не вызывает кинематику
    FStatic: Boolean;
    // Настраивоемое событие, когда происходит пересечение
    FOnIntersection: TOnIntersection;
    // Расширенная информация о теле: сюда можно записывать что угодно
    FEx: TObject;
    // Позиция тела
    FPos: TVec2f;
    // Предыдущая позиция тела
    //   Может быть полезна для быстродвижущихся тел.
    //   Например, если есть пуля, то для пересечения с другими объектами
    //   нужно знать какую траеторию она заметает
    FLastPos: TVec2f;
    // Скорость
    FVel: TVec2f;
    // Ускорение
    FAcc: TVec2f;
    // Для удобства: находится ли тело на земле, по флагу определяем 
    // нужно ли применять планетарную гравитацию
    FOnGround: Boolean;
    // Толчки на тело
    FImpacts: array of TImpact;
    // Базовый конструктор
    constructor Create(World: TWorld; 
                       _Static: Boolean;
                       OnIntersection: TOnIntersection);
    // Проверяет пересекается ли тело с другим телом
    //   Other - тело, с которым нужно проверить пересечение
    //   Point - точка, по которой они пересеклись (при Result=True)
    //   Result - пересеклись ли тела
    function Intersect(Other: TBody; out Point: TVec2f): Boolean; virtual;
    // Проверяет пересекается ли тело с лучом
    //     Pos - вершина луча
    //     Dir - направление луча
    //     Point - точка, в которой луч пересекает тело (при Result=True)
    //     Result - было ли пересечение
    function RayIntersect(const Pos, Dir: TVec2f; 
                            out Point: TVec2f): Boolean; virtual;
    // Применение кинематики: обновляем скорость и положение
    //     Grav - внешняя гравитация
    //     DeltaTime - промежуток времени в миллисекундах, в течении которого
    //     происходит воздействие на тело
    procedure DoKinematics(DeltaTime: Integer);
    // Пнуть тело
    procedure Kick(const Acc: TVec2f; Time: Integer);
    // Полностью остановить тело: обнулить скорость и воздействия на него
    procedure Stop;

    property World: TWorld read FWorld;
    property Static: Boolean read FStatic;
    property OnIntersection: TOnIntersection read FOnIntersection write FOnIntersection;
    property Ex: TObject read FEx write FEx;
    property Pos: TVec2f read FPos write FPos;
    property LastPos: TVec2f read FLastPos;
    property Vel: TVec2f read FVel write FVel;
    property Acc: TVec2f read FAcc;
    property OnGround: Boolean read FOnGround;
  end;

  TBodyLine = class(TBody)
    // TODO: переопределить Intersect и RayIntersect
  end;

  TBodyCircle = class(TBody)
    // TODO: переопределить Intersect и RayIntersect
  end;

  TGravitation = class
    // Воздействует ли гравитация на тело
    function IsEffectOnBody(Body: TBody): Boolean; virtual;
    // Сила гравитации на заданное тело
    function GetValue(Body: TBody): TVec2f; virtual;
  end;

  TWorld = class
    // Все тела в мире
    FBodies: array of TBody;
    // Гравитации
    FGravitations: array of TGravitation;
    // Применить гравитации
    procedure ApplyGravitations;
    // Применение кинематики: обновляем скорость и положение тел
    procedure DoKinematics(DeltaTime: Integer);
    // Проверка пересечений
    procedure DoIntersections;
    // Полная обработка физики
    procedure Update(DeltaTime: Integer);
    // Добавляем тело
    procedure Add(Body: TBody); overload;
    procedure Add(Grav: TGravitation); overload;
  end;

// Обработчик пересечения, который ничего не делает
// Полезен для стен
procedure DoIntersectionStatic(World: TWorld; Body, Other: TBody; const Point: TVec2f);
// "Пинающий" обработчик: Body отлетит от точки столкновения так, как будто это мячик
procedure DoIntersectionKick(World: TWorld; Body, Other: TBody; const Point: TVec2f);
// Можно написать другие обработчики: 
//   "Пластилиновый обработчик": Body сразу тормозится в точке столкновения
//   

implementation

constructor TBody.Create;
begin
  FWorld := World;
  FStatic := _Static;
  FWorld.Add(Self);
  FOnIntersection := OnIntersection;
  // TODO: очистить все значения
end;

function TBody.Intersect(Other: TBody; out Point: TVec2f): Boolean;
begin
  Result := False;
end;

function TBody.RayIntersect(const Pos, Dir: TVec2f; 
                        out Point: TVec2f): Boolean;
begin
  Result := False;
end;

procedure TBody.DoKinematics(DeltaTime: Integer);
var
  I, T: Integer;
  NewVel: TVec2f;
begin
  // Считаем новую скорость
  NewVel := Mul2f(FAcc, DeltaTime);
  // Применяем толчки
  for I := 0 to High(FImpacts) do begin
    T := Min(DeltaTime, FImpacts[I].Time);
    NewVel := NewVel + Mul2f(FImpacts[I].Time, T);
    Dec(FImpacts[I].Time, T);
    if FImpacts[I].Time = 0 then begin
      // TODO: удалить FImpacts[I] из списка
    end;
  end;
  // Запоминаем старую позицию
  FLastPos := FPos;
  // Считаем новую позицию, с учётом средней скорости
  FPos := FPos + Mul(FVel + NewVel, 0.5);
  // Записываем новую скорость
  FVel := NewVel;
end;

procedure TBody.Kick(const Acc: TVec2f; Time: Integer);
begin
  SetLength(FImpacts, Length(FImpacts) + 1);
  FImpacts[High(FImpacts)].Acc := Acc;
  FImpacts[High(FImpacts)].Time := Time;
end;

procedure TBody.Stop;
begin
  FVel := Vec2f(0, 0);
  SetLength(FImpacts, 0);
end;

procedure TWorld.ApplyGravitations;
var
  I, J: Integer;
begin
  for I := 0 to High(FBodies) do
    FBodies[I].Acc := Vec2f(0, 0);
  for I := 0 to High(FGravitations) do
    for J := 0 to High(FBodies) do
      if FGravitations[I].IsEffectOnBody(FBodies[J]) then
        FBodies[J].Acc := FBodies[J].Acc + FGravitations[I].GetValue(FBodies[J]);
end;

procedure TWorld.DoKinematics(DeltaTime: Integer);
var
  I: Integer;
begin
  for I := 0 to High(FBodies) do
    if not FBodies[I].IsStatic then
      FBodies[I].DoKinematics(DeltaTime);
end;

procedure TWorld.DoIntersections;
var
  I, J: Integer;
  Point: TVec2f;
begin
  for I := 0 to High(FBodies) do
    for J := 0 to High(FBodies) do
      if FBodies[I].Intersect(FBodies[J], Point) or 
         FBodies[J].Intersect(FBodies[I], Point) then begin
        if @FBodies[I].OnIntersection <> nil then
          FBodies[I].OnIntersection(Self, FBodies[I], FBodies[J], Point);
        if @FBodies[J].OnIntersection <> nil then
          FBodies[J].OnIntersection(Self, FBodies[J], FBodies[I], Point);
      end;
end;

procedure TWorld.Update(DeltaTime: Integer);
begin
  ApplyGravitations;
  // Сперва обрабатываем столкновения
  DoIntersections;
  // Потом обрабатываем кинематику
  DoKinematics(DeltaTime);
end;

procedure TWorld.Add(Body: TBody);
begin
  SetLength(FBodies, Length(FBodies) + 1);
  FBodies[High(FBodies)] := Body;
end;

procedure TWorld.Add(Grav: TGravitation);
begin
  SetLength(FGravitations, Length(FGravitations) + 1);
  FGravitations[High(FGravitations)] := Grav;
end;

function TGravitation.IsEffectOnBody(Body: TBody): Boolean; 
begin
  Result := True;
end;

function TGravitation.GetValue(Body: TBody): TVec2f; 
begin
  Result := Vec2f(0, 0);
end;


procedure DoIntersectionStatic(World: TWorld; Body, Other: TBody; const Point: TVec2f);
begin
end;

procedure DoIntersectionKick(World: TWorld; Body, Other: TBody; const Point: TVec2f);
var
  Delta, ForwardVel: TVec2f;
begin
  // находим вектор из Body.Pos в Point
  Delta := Point - Body.Pos;
  // нормализуем его
  Delta := Norm2f(Delta);
  // находим скорость на точку столкновения
  ForwardVel := Mul2f(Delta, Dot2f(Body.Vel, Delta));
  // отражаем её и удваиваем
  ForwardVel := Vec2f(- 2*ForwardVel.X, - 2*ForwardVel.Y);
  // прибавляем к скорости тела
  Body.Vel := Body.Vel + ForwardVel;
end;

end.
